package cn.lysoy.demo.aspect;

import cn.lysoy.demo.annotation.AutoIdempotent;
import cn.lysoy.demo.constant.RespBeanEnum;
import cn.lysoy.demo.exception.GlobalException;
import cn.lysoy.demo.utils.RedisLock;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.UUID;

/**
 * 自动幂等切面
 * @author SongYang
 */
@Slf4j
@Aspect
@Component
@ConditionalOnClass({RedisLock.class, AutoIdempotent.class})
public class AutoIdempotentAspect {
    /**
     * redis缓存key的模板
     */
    private static final String KEY_TEMPLATE = "idempotent:%s";

    private final RedisLock redisLock;


    @Autowired
    public AutoIdempotentAspect(RedisLock redisLock) {
        this.redisLock = redisLock;
    }

    /**
     * 根据实际路径进行调整
     */
    @Pointcut("@annotation(cn.lysoy.demo.annotation.AutoIdempotent)")
    public void executeIdempotent() {
    }

    @Around("executeIdempotent()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //获取幂等注解
        AutoIdempotent idempotent = method.getAnnotation(AutoIdempotent.class);
        //根据 key前缀 + @Idempotent.value() + 方法签名 + 参数 构建缓存键值
        //确保幂等处理的操作对象是：同样的 @Idempotent.value() + 方法签名 + 参数
        //调用KeyUtil工具类生成key
        final String key = String.format(KEY_TEMPLATE, idempotent.value() + ":" + generateKey(method, joinPoint.getArgs()));
        if (idempotent.flag()) {
            //通过setnx确保只有一个接口能够正常访问
            String uuid = UUID.randomUUID().toString();
            boolean lock = redisLock.lock(key, uuid, idempotent.expireMillis());
            if (lock) {
                Object proceed = joinPoint.proceed();
                // 执行完成后释放锁
                redisLock.unlock(key, uuid);
                return proceed;
            }else {
                log.debug("获取锁失败, key:" + key+";加锁原因:"+idempotent.reason());
                throw new GlobalException(RespBeanEnum.ACCESS_LIMIT_ERROR);
            }
        }else {
            //通过setnx确保只有一个接口能够正常访问
            boolean lock = redisLock.lock(key, idempotent.expireMillis());
            if (lock) {
                Object proceed = joinPoint.proceed();
                // 执行完成后释放锁
                redisLock.unlock(key);
                return proceed;
            }else {
                log.debug("获取锁失败, key:" + key+";加锁原因:"+idempotent.reason());
                throw new GlobalException(RespBeanEnum.ACCESS_LIMIT_ERROR);
            }
        }
    }

    /**
     * 根据{方法名 + 参数列表}和md5转换生成key
     */
    private String generateKey(Method method, Object... args) {
        StringBuilder sb = new StringBuilder(method.toString());
        for (Object arg : args) {
            String argString;
            if (arg instanceof Number) {
                argString  = arg.toString();
            }else {
                argString = JSON.toJSONString(arg);
            }
            sb.append(argString);
        }
        return DigestUtils.md5Hex(sb.toString());
    }
}
